package implementation1.app.commands;

import exceptions.CommandErrorFromUserException;
import exceptions.CommandException;
import exceptions.StreamException;
import implementation1.app.Global;
import implementation1.app.streams.AccessAttributes;
import implementation1.data.states.TerminatedStatus;
import interfaces.app.IProcess;
import interfaces.data.IHandle;

import java.io.File;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Class for handle command "ls" for showing files in actual directory.
 * Created: 15.10.11
 *
 * @author Jan Svab
 */
public class Ls extends Command
{

    private boolean longFormat = false;
    private boolean humanReadable = false;
    private boolean listAll = false;

    /**
     * Default constructor.
     *
     * @param parent parent process over which is command processed
     * @param handle parameters of command
     */
    public Ls(IProcess parent, IHandle handle)
    {
        super(parent, handle);

    }

    /**
     * Main method for processing of command.
     *
     * @param args arguments of command
     * @throws CommandException Thrown if occurs an error during processing of the command.
     */
    public void mainCommand(String[] args) throws CommandException
    {
        if (handle.getOut() == null)
        {
            throw new CommandException(bundle.getString("error.outputStream.empty"));
        }

        setProperties(args);

        //counts dir links
        int dirsCount = 0;
        for (String arg : args)
        {
            if (arg.startsWith("-")) continue;
            dirsCount++;
        }

        File dir;

        //if default dir
        if (args == null || args.length == 0 || dirsCount == 0)
        {
            dir = new File(handle.getActualPath());
            listFiles(dir, false);
        }
        else
        {
            //list of dirs
            for (String arg : args)
            {
                if (arg.startsWith("-")) continue;

                dir = Global.getFileWithAbsPath(handle.getActualPath(), arg);

                if (!dir.exists() || !dir.isDirectory())
                {
                    throw new CommandErrorFromUserException(bundle.getString("error.ls.directory"));
                }

                listFiles(dir, (dirsCount > 1));
            }
        }
    }

    /**
     * Sets command properties according to passed arguments.
     *
     * @param args The arguments.
     */
    private void setProperties(String[] args)
    {
        for (String arg : args)
        {
            if (arg.startsWith("-"))
            {
                if (arg.indexOf("l") >= 1) longFormat = true;
                if (arg.indexOf("a") >= 1) listAll = true;
                if (arg.indexOf("h") >= 1) humanReadable = true;
            }
        }
    }

    /**
     * Lists files in selected directory and writes their attributes to the output.
     *
     * @param dir          Selected directory.
     * @param writeDirName Determines, whether the name of listed dir should be written.
     * @throws CommandException When error while writing into output has occurred.
     */
    private void listFiles(File dir, boolean writeDirName) throws CommandException
    {
        try
        {
            handle.getOut().open(AccessAttributes.WRITE);
            openedStreamsOfProcess.add(handle.getOut());

            File[] files = dir.listFiles();
            if (writeDirName) handle.getOut().writeLine("\n" + dir.getName() + ":");

            for (File f : files)
            {
                if (Thread.interrupted()) //in other process was created request to terminate this process
                {
                    status = new TerminatedStatus();
                    break;
                }

                if (hasEndStatus()) //thread should quit
                {
                    break;
                }

                if (!f.getName().startsWith(".") || listAll) handle.getOut().writeLine(formatOutput(f));
            }
        } catch (StreamException e)
        {
            throw new CommandException(e.getMessage());
        } finally
        {
            Global.closeStreamQuietly(handle.getOut());
            openedStreamsOfProcess.remove(handle.getOut());
        }
    }

    /**
     * Gets attributes from the file and formats it according to arguments (-l, -h).
     *
     * @param file The file.
     * @return The attributes.
     */
    private String formatOutput(File file)
    {
        if (!longFormat) return file.getName();
        return formatAttributes(file) + " " + formatSize(file.length()) + " " + formatFileTime(file) + " " + file.getName();
    }

    /**
     * Formats file last modified time.
     *
     * @param file The file.
     * @return Formated date-time.
     */
    private String formatFileTime(File file)
    {
        Date lm = new Date(file.lastModified());
        return new SimpleDateFormat("yyyy-MM-dd hh:mm").format(lm);
    }

    /**
     * Formats file attributes (read, write, execute).
     *
     * @param file The file.
     * @return Formatted attributes.
     */
    private String formatAttributes(File file)
    {
        return (file.isDirectory() ? "d" : "-") + (file.canRead() ? "r" : "-") + (file.canWrite() ? "w" : "-") + (file.canExecute() ? "x" : "-");
    }

    /**
     * Format file size according to arguments (-h).
     *
     * @param bytes size of file
     * @return string with file size in required format
     */
    private String formatSize(long bytes)
    {
        if (!humanReadable) return String.format("%8s", bytes + "B");

        double size = bytes;
        int cycles = 0;
        while (size >= 1000)
        {
            cycles++;
            size /= 1024.0;
        }

        String fileSize;

        switch (cycles)
        {
            case 1:
                fileSize = new DecimalFormat("#.#").format(size) + "kB";
                break;
            case 2:
                fileSize = new DecimalFormat("#.#").format(size) + "MB";
                break;
            case 3:
                fileSize = new DecimalFormat("#.#").format(size) + "GB";
                break;
            default:
                fileSize = bytes + "B";
                break;
        }

        return String.format("%7s", fileSize);
    }

    @Override
    public String toString()
    {
        return "ls";
    }
}
